﻿/*
   Created 2009 March 15 Randall Maas
   Copyright (c) 2009, Randall Maas
   All rights reserved.

   Redistribution and use in source and binary forms, with or without
   modification, are permitted provided that the following conditions are met:

   Redistributions of source code must retain the above copyright notice, this
   list of conditions and the following disclaimer.

   Redistributions in binary form must reproduce the above copyright notice,
   this list of conditions and the following disclaimer in the documentation
   and/or other materials provided with the distribution.

   Neither the name of Blackwood Designs, LLC nor the names of its contributors
   may be used to endorse or promote products derived from this software without
   specific prior written permission.

   THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
   AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
   IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
   ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
   LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
   CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
   SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
   INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 
   CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
   ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
   THE POSSIBILITY OF SUCH DAMAGE.

   Portions Modified from example:
   http://social.msdn.microsoft.com/Forums/en-US/gametechnologiesdirectx101/thread/b217416f-c1bc-4df8-aec7-754cc41391f5/
 */


using System;
using System . Threading;
using System . Windows . Forms;
using Microsoft . DirectX . DirectSound;
using System . Net . Sockets;
using System . Diagnostics;

/// <summary>
/// Class to interface with the microphone
/// </summary>
static partial class Microphone
{
   #region Operating Parameters
   /// <summary>
   /// The sample window defaults to a sample size of about 15ms
   /// </summary>
   const double WindowDuration = 0.010;

   /// <summary>
   /// The sample rate we should use; The system expects either 16000 or 44100
   /// </summary>
   internal static int SampleRate = 44100;

   static int _Volume = -300;
   internal static int Volume
   {
      set
      {
         _Volume = value;
         if (null != playbackBuffer)
           try{playbackBuffer . Volume = value > -10000?value:-10000;}
           catch{}
      }
   }

   /// <summary>
   /// The control we will have playback cooperate with
   /// </summary>
   static Control _ReferenceControl;
   internal static Control ReferenceControl
   {
      set
      {
         playbackDevice.SetCooperativeLevel(_ReferenceControl = value, CooperativeLevel.Priority);
      }
   }
   #endregion

   #region Internal Variables
   /// <summary>
   /// The number of bytes in a buffer.  This is typically 5ms of sound
   /// </summary>
   static int bufferSize;
   const int _bufferPositions = 8;

   /// <summary>
   /// The thread that plays the audio in the background
   /// </summary>
   static Thread  echoThread;


   /// <summary>
   /// A separate wait-event for each slot in the buffer
   /// </summary>
   /// <remarks>
   /// We need a separate one for each slot since this aren't countable.  That is
   /// if second slot is ready before we process the first, we'll fall one slot
   /// behind, and not be able to catch up.  This is latency, which is really
   /// noticable to the ear
   /// </remarks>
   static AutoResetEvent[] notificationEvent = new AutoResetEvent[_bufferPositions];

   static volatile bool Runnable=false;

   static FullDuplex fullDuplex;

   /// <summary>
   /// The sound device
   /// </summary>
   /// <remarks>
   /// Debugging in Visual Studio will throw up an MDA saying that a LoaderLock
   /// violation occurred.  Ignore it
   /// </remarks>
   static Capture captureDevice = new Capture();

   /// <summary>
   /// The buffer where the sample sounds are placed
   /// </summary>
   internal static CaptureBuffer captureBuffer;


   /// <summary>
   /// The play back device we be using for the side tones
   /// </summary>
   /// <remarks>
   /// We create it early so that we can set its cooperation at the start of
   /// execution.  Otherwise we often get cross-thread exceptions
   /// </remarks>
   static Device playbackDevice = new Device();

   /// <summary>
   /// The buffer used to play sound back from
   /// </summary>
   internal static SecondaryBuffer playbackBuffer;
   #endregion

   #region Butterworth Low Pass Filter
   /// <summary>
   /// Butterworth state variables.
   /// </summary>
   /// <remarks>
   /// These are needed since our sample window is so small.
   /// Without these, a sample windows of (say) 10ms, the filter
   /// wouldn't know about sounds less than an audible frequency, and
   /// starting with 0's with each sample window introduces a bit of
   /// crackle
   /// </remarks>
   static double O_1 = 0.0, O_2=0.0;

   /// <summary>
   /// Implements a butterworth filter
   /// </summary>
   /// <param name="InBuffer">The incoming samples</param>
   /// <param name="Ofs">The start of the samples in the buffer (before this are old samples)</param>
   /// <param name="OutBuffer">The filtered samples</param>
   /// <param name="Coefs">The coefficients for an order-2 filter</param>
   static void Butterworth(Int16[] InBuffer, int Ofs, byte[] OutBuffer, double[] Coefs)
   {
      double C0=Coefs[0], C1=Coefs[1], C2 = Coefs[2], C3=Coefs[3];
      double I_1=InBuffer[Ofs-1], I_2= InBuffer[Ofs-2];
      for (int L = InBuffer . Length, J=0, I = Ofs; I < L; I++)
      {
         double I_0 = InBuffer[I];

         // Filter the samples
         double A = (I_0 + I_2) * C0 + I_1 * C1;
         I_2 = I_1;
         I_1 = I_0;

         A = A - O_1 * C2 - O_2 * C3;
         O_2 = O_1;
         O_1 = A;

         // Convert it back to 16 bit
         Int16 S;
         if (A < -32767) S = -32767;
         if (A > 32767)  S = 32767;
         else S = (Int16) A;

         // Store it
         OutBuffer[J++] = (byte)(S & 0xFF);
         OutBuffer[J++] = (byte)(((UInt16)S >> 8) & 0xFF);
      }
   }
   #endregion


   /// <summary>
   /// This is the main loop of sampling the microphone and passing it on
   /// </summary>
   static void MicrophoneLoop()
   {
      int Idx = 0;
      // A temporary buffer to hold decoded PCM samples
      Int16[] PCM16Buffer = new Int16[10+bufferSize/2];

      // The low pass results
      byte[]  LPSample    = new byte[bufferSize];

      // The low-pass (muffle) filter coefficients
      double[] Coefs = null;
      switch (SampleRate)
      {
         case 48000: Coefs = GainControl.LP_1k_48k; break;
         case 44100: Coefs = GainControl.LP_1k_44k; break;
         case 16000: Coefs = GainControl.LP_1k_16k; break;
      }

      // Start playing after we've queued the first sample
      bool playing = false;
      captureBuffer.Start(true);

      try
      {
         while (Runnable)
          for (int I = 0, offset=0; Runnable && I < _bufferPositions; I++, offset+= bufferSize)
          {
             // Wait for the sample areas to be ready
             notificationEvent[I] . WaitOne(Timeout.Infinite, true);

             // Get the sound samples
             byte[] buffer = (byte[]) captureBuffer.Read(offset, typeof(byte), LockFlag.None, bufferSize);

             // Convert samples to 16bit PCM 
             for (int L = buffer . Length, J=10, K = 0; K < L; K += 2)
              PCM16Buffer[J++] = (Int16) ((UInt16)buffer[K] | (((UInt16)buffer[K +1]) << 8));

             #region Sidetone
             // Play them out to the ear, if applicable
             if (null != playbackBuffer)
               {
                  // Perform a low pass filter to "muffle" the sound
                  Butterworth(PCM16Buffer, 10, LPSample, Coefs);

                  // put the muffled sample into the output buffer
                  // -- The lock flag seems to work, but others may work too
                  playbackBuffer . Write(Idx, LPSample, LockFlag.None);
                  Idx += buffer . Length;
                  if (Idx >= 4*bufferSize)
                    Idx -= 4*bufferSize;
                  if (!playing)
                    {
                       playbackBuffer . Volume = _Volume;
                       playbackBuffer . Play(0,  BufferPlayFlags.Looping);
                       playing = true;
                    }
               }
             #endregion

             // Process the sound and deliver it to Skype
             if (null != outStream)
               {
                  int L = AGC . Process(PCM16Buffer, 10, buffer);
                  if (0 != L)
                    outStream.BeginSend(buffer, 0, L, SocketFlags.None, SendCallback, null);
                  // Note: could send out pink noise if L == 0
               }


             // Move the sliding window of the previous 10 samples into the start
             // of the PCM16Buffer
             for (int K = 0, J = PCM16Buffer.Length-10; K < 10; K++,J++)
              PCM16Buffer[K] = PCM16Buffer[J];
          }
      }
      catch{}

      // Release resources we are using
      if (null != playbackBuffer)
        {
           playbackBuffer . Stop();
           playbackBuffer . Dispose();
        }
      playbackBuffer= null;

      if (null != captureBuffer)
        captureBuffer.Dispose();
      captureBuffer = null;

      for (int I = 0; I < notificationEvent . Length; I++)
       if (null != notificationEvent[I])
         {
            notificationEvent[I] . Close();
            notificationEvent[I] = null;
         }

      if (null != fullDuplex)
        fullDuplex . Dispose();
      fullDuplex = null;
   }

   static void SendCallback(IAsyncResult ar)
   {
      outStream . EndSend(ar);
   }

   #region Buffer Description
   static WaveFormat WFormat()
   {
      //Set up the wave format to be captured
      WaveFormat waveFormat = new WaveFormat();
      waveFormat.Channels         = 1;
      waveFormat.FormatTag        = WaveFormatTag.Pcm;
      waveFormat.SamplesPerSecond = SampleRate; // Because that is what Skype wants
      waveFormat.BitsPerSample    = 16;
      waveFormat.BlockAlign       = 2;
      waveFormat.AverageBytesPerSecond = waveFormat.BlockAlign * waveFormat.SamplesPerSecond;
      return waveFormat;
   }
   #endregion


   /// <summary>
   /// The automatic gain control module, used to increase the volume from the
   /// microphone
   /// </summary>
   internal static GainControl AGC;

   /// <summary>
   /// Starts the microphone background thread
   /// </summary>
   /// <param name="UseSideTones">true to play the microphone out the speakers, false otherwise</param>
   public static void StartMicrophone(bool UseSideTones)
   {
      WaveFormat waveFormat = WFormat();

      // Calculate the length of the buffer (it must be an even number of samples
      bufferSize = (int)(waveFormat.AverageBytesPerSecond * WindowDuration);
      if (bufferSize % 2 != 0)
        bufferSize++;

      AGC = new GainControl(SampleRate, bufferSize/2);

      CaptureBufferDescription captureBufferDescription = new CaptureBufferDescription();
      captureBufferDescription.BufferBytes = _bufferPositions * bufferSize;
      captureBufferDescription.Format      = waveFormat;

      // Setup playback buffer if we're using side tones
      BufferDescription playbackBufferDescription = null;
      if (UseSideTones)
        {
           playbackBufferDescription = new BufferDescription(waveFormat);
           playbackBufferDescription.BufferBytes           = 4* bufferSize;
           playbackBufferDescription.ControlPositionNotify = true;//**
           playbackBufferDescription.ControlPan = true;//**
           playbackBufferDescription.ControlFrequency = true;//**
           playbackBufferDescription.GlobalFocus           = true;
           playbackBufferDescription.StickyFocus           = true;
           playbackBufferDescription.ControlVolume         = true;
           playbackBufferDescription.DeferLocation         = true;
           playbackBufferDescription.CanGetCurrentPosition = true;
        }

#if false
// for some reason this won't let me play sound (in the thread)
// I left it here in case someone can make it work
      // Enable noise cancellation and filter if 16Khz
      if (16000 == SampleRate && UseSideTones)
        {
           // DirectSound (according to docs) only supports this at 16Khz
           CaptureEffectDescription[] Effects   = new CaptureEffectDescription[2];
           Effects[0] . LocateInSoftware        = true;
           Effects[0] . GuidEffectsClass        = DSoundHelper.CaptureEffectsClassAcousticEchoCancellation;
           Effects[0] . GuidEffectsInstance     = DSoundHelper.CaptureEffectsSystemAcousticEchoCancellation;
           Effects[1] . LocateInSoftware        = true;
           Effects[1] . GuidEffectsClass        = DSoundHelper.CaptureEffectsClassNoiseSuppression;
           Effects[1] . GuidEffectsInstance     = DSoundHelper.CaptureEffectsSystemNoiseSuppression;
           captureBufferDescription.CaptureEffectDescription = Effects;
           captureBufferDescription.ControlEffects  = true;
           playbackBufferDescription.ControlEffects = true;
           fullDuplex = new FullDuplex(captureBufferDescription, playbackBufferDescription,
                                       _ReferenceControl,        CooperativeLevel.Priority,
                                       ref captureBuffer,        ref playbackBuffer);
        }
       else
#endif
        {
           captureBuffer = new CaptureBuffer(captureBufferDescription, captureDevice);
           if (UseSideTones)
             playbackBuffer = new SecondaryBuffer(playbackBufferDescription, playbackDevice);
        }

      // Create the notifications of the sampels are ready
      BufferPositionNotify[] positionNotify = new BufferPositionNotify[_bufferPositions + 1];

      for (int i = 0; i < _bufferPositions; i++)
      {
         positionNotify[i].Offset = bufferSize * (i+1) - 1;
         notificationEvent[i] = new AutoResetEvent(false);
         positionNotify[i].EventNotifyHandle = notificationEvent[i].SafeWaitHandle.DangerousGetHandle();
      }
      Notify N = new Notify(captureBuffer);
      N . SetNotificationPositions(positionNotify, _bufferPositions);
      N . Dispose();

      Runnable=true;
      echoThread = new Thread(new ThreadStart(MicrophoneLoop));
      echoThread . Priority = ThreadPriority . Highest;
      echoThread . Start();
   }


   /// <summary>
   /// This is used to stop the Microphone capture and processing resources
   /// and threads.
   /// </summary>
   internal static void StopMicrophone()
   {
      if (!Runnable || null == echoThread)
        return ;

      // Stop the background with a "handshake"
      //  * We set Runnable to false
      //  * The background thread sets it true when it stops
      //  * We wait for it to stop
      //  * Then we clear the thread reference
      Runnable = false;
      echoThread .  Join();
      echoThread = null;
   }
}
